export function isObject(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
};

export function isPlainObject(value) {
    return typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
};

export function isFunction(value) {
    return typeof value === "function";
};

export function isArray(value) {
    return Array.isArray(value);
};

export function isString(value) {
    return typeof value === "string";
};

export function isNumber(value) {
    return typeof value === "number" && !Number.isNaN(value) && Number.isFinite(value);
};

export function isBool(value) {
    return typeof value === "boolean";
};

export function isInvalid(value) {
    return value === undefined || value === null;
};

export function isPromise(value) {
    return value instanceof Promise;
};

export function isError(value) {
    return value instanceof Error;
};

export function object(value) {
    if (isObject(value)) {
        return value;
    } else if (isFunction(value)) {
        try {
            return object(value());
        } catch (error) {
            return {};
        }
    } else if (isString(value) && /^\s*\{[\s\S]*\}\s*$/.test(value)) {
        return object(() => JSON.parse(value));
    } else {
        return {};
    }
};

export function func(value) {
    if (isFunction(value)) {
        return value;
    } else if (isString(value)) {
        return new Function(value);
    } else {
        return new Function();
    }
};

export function array(value) {
    if (isArray(value)) {
        return value;
    } else if (isObject(value)) {
        return [].slice.call(value);
    } else if (isFunction(value)) {
        try {
            return array(value());
        } catch (error) {
            return [];
        }
    } else if (isString(value) && /^\s*\[[\s\S]*\]\s*$/.test(value)) {
        return array(() => JSON.parse(value));
    } else {
        return [];
    }
};

export function string(value) {
    if (isString(value)) {
        return value;
    } else if (isFunction(value)) {
        try {
            return string(value());
        } catch (error) {
            return "";
        }
    } else if (isInvalid(value)) {
        return "";
    } else {
        return "" + value;
    }
};

export function int(value, power) {
    if (isString(value) || isNumber(value)) {
        value = "" + (parseFloat(value) || 0);
        value = /^-?\d+(\.\d+)?$/.test(value) ? value.split(".").concat("") : ["0", "0"];
        power = isNumber(power) ? Math.floor(Math.min(Math.max(power, 0), 6)) : 0;
        value = parseInt(value[0] + (value[1] + "000000").substr(0, power));
        return Number.isSafeInteger(value) ? value : 0;
    } else if (isFunction(value)) {
        try {
            return int(value(), power);
        } catch (error) {
            return 0;
        }
    } else {
        return 0;
    }
};

export function float(value, decimals) {
    if (isString(value) || isNumber(value)) {
        value = "" + (parseFloat(value) || 0);
        value = /^-?\d+(\.\d+)?$/.test(value) ? value.split(".").concat("") : ["0", "0"];
        decimals = isNumber(decimals) ? Math.floor(Math.min(Math.max(decimals, 0), 6)) : 6;
        value = parseFloat(value[0] + "." + (value[1] + "000000").substr(0, decimals));
        return Number.isSafeInteger(value >= 0 ? Math.ceil(value) : Math.floor(value)) ? value : 0;
    } else if (isFunction(value)) {
        try {
            return float(value(), decimals);
        } catch (error) {
            return 0;
        }
    } else {
        return 0;
    }
};

export function bool(value) {
    if (isBool(value)) {
        return value;
    } else if (isFunction(value)) {
        try {
            return bool(value());
        } catch (error) {
            return false;
        }
    } else {
        return !!value;
    }
};

export function findValue(list, sourceKeyName, sourceKeyValue, destKeyName, destKeyValue) {
    if (!isString(sourceKeyName)) sourceKeyName = string(sourceKeyName);
    if (!isString(destKeyName)) destKeyName = string(destKeyName);
    let data = array(list).find((data) => !isInvalid(data) && data[sourceKeyName] === sourceKeyValue);
    return isInvalid(data) ? destKeyValue : data[destKeyName];
};

export function traverse(source, children, handler) {
    let ended = false;
    let result = undefined;
    let traverse = (source, children, handler, parent) => {
        for (let index = 0; index < source.length; index++) {
            if (ended) break;
            if (bool(handler(source[index], index, source, parent))) {
                ended = true;
                result = source[index];
            } else {
                traverse(array(() => source[index][children]), children, handler, source[index]);
            }
        }
    };
    return traverse(array(source), string(children), func(handler)), result;
};

export function deepCopy(source, excludes) {
    let copy = (source, parents, excludes) => {
        if (isString(source) || isNumber(source) || isBool(source) || isInvalid(source)) {
            return source;
        } else if (isPlainObject(source)) {
            if (parents.includes(source)) return;
            else parents.push(source);
            let temp = Object.keys(source).reduce((temp, key) => {
                if (excludes.includes(key)) { }
                else temp[key] = copy(source[key], parents, excludes);
                return temp;
            }, {});
            return parents.pop(), temp;
        } else if (isArray(source)) {
            if (parents.includes(source)) return;
            else parents.push(source);
            let temp = source.map((source) => copy(source, parents, excludes));
            return parents.pop(), temp;
        } else {
            return null;
        }
    };
    return copy(source, [], array(excludes));
};

export function delInvalidProp(value) {
    return isObject(value) && Object.keys(value).forEach((key) => {
        if (isInvalid(value[key])) delete value[key];
    }), value;
};

export function erase(...values) {
    if (values.length > 1) {
        return values.map((value) => erase(value));
    } else if (values.length > 0) {
        let value = values[0];
        if (isObject(value)) Object.keys(value).forEach((key) => value[key] = erase(value[key]));
        else if (isArray(value)) value.splice(0);
        else if (isString(value)) value = "";
        else if (isNumber(value) || typeof value === "number") value = 0;
        else if (isBool(value)) value = false;
        else value = null;
        return value;
    }
};

export function hook(fn, handler) {
    return function(...args) {
        return func(handler).call(this, args, (args) => func(fn).apply(this, array(args)));
    };
};

export function call(fn, context, ...args) {
    return func(fn).apply(context, args);
};

export function wrapFn(fn, context, ...args1) {
    return function(...args2) {
        return func(fn).call(context, ...args1, ...args2);
    };
};

export function randomInt(min, max) {
    let limit = Number.MAX_SAFE_INTEGER;
    let start = isInvalid(min) ? 0 : Math.floor(Math.min(Math.max(int(min), 0), limit));
    let end = isInvalid(max) ? limit : Math.floor(Math.min(Math.max(int(max), start), limit));
    return Math.floor((end - start + 1) * Math.random()) + start;
};

export function replaceEmoji(value, replacer) {
    return string(value).replace(/[\uD800-\uDFFF]/g, string(replacer));
};

export function toTimeString(value, template) {
    let date = new Date(value);
    let keys = ["YYYY", "Y", "MM", "M", "DD", "D", "hh", "h", "mm", "m", "ss", "s", "nn", "n", "W"];
    let pad = (value, length) => string(value).padStart(length, "0");
    let values = [pad(date.getFullYear(), 4), date.getFullYear(), pad(date.getMonth() + 1, 2), date.getMonth() + 1, pad(date.getDate(), 2), date.getDate(), pad(date.getHours(), 2), date.getHours(), pad(date.getMinutes(), 2), date.getMinutes(), pad(date.getSeconds(), 2), date.getSeconds(), pad(date.getMilliseconds(), 3), date.getMilliseconds(), "日一二三四五六".substr(date.getDay(), 1)];
    return isNumber(date.getTime()) && date.getTime() > 0 ? keys.reduce((result, key, index) => result.replace(new RegExp(key, "g"), values[index]), string(template)) : "";
};

export function toMoneyString(value, thousands, prefix) {
    let number = float(value);
    value = string(Math.abs(number)).split(".").concat("");
    value[0] = value[0].split("").reverse().reduce((arr, data, i, list) => {
        arr.push(data);
        if (i % 3 === 2 && i + 1 !== list.length) arr.push(string(thousands));
        return arr;
    }, []).reverse().join("");
    value[1] = "." + (value[1] + "00").substr(0, 2);
    return (number >= 0 ? "" : "-") + string(prefix) + value[0] + value[1];
};

export function toMagicImgURL(value, width = 256, height = 256, cover = true, cut = false) {
    let [prefix, search] = string(value).split("#")[0].split("?");
    if (prefix === "") return "";
    if (isNumber(width)) width = Math.min(Math.max(int(width), 1), 4096);
    if (isNumber(height)) height = Math.min(Math.max(int(height), 1), 4096);
    let aliyun = /\.aliyuncs\.com\//i.test(prefix);
    let query = string(search).split("&").reduce((list, data) => {
        let [key, value] = data.split("=");
        if (key === "aliyun") aliyun = true;
        if (key !== "" && key.indexOf("imageMogr2/") !== 0 && key !== "x-oss-process") list.push(data);
        return list;
    }, []);
    if (aliyun) {
        if (cover) query.push("x-oss-process=image/format,jpg/resize,m_mfit,w_" + width + ",h_" + height + ",limit_0" + (cut ? "/crop,w_" + width + ",h_" + height + ",g_center" : ""));
        else query.push("x-oss-process=image/format,jpg/resize,w_" + width + ",h_" + height + ",limit_0");
    } else {
        if (/\.gif$/i.test(prefix)) query.push("imageMogr2/cgif/1");
        else if (cover) query.push("imageMogr2/format/jpg/thumbnail/!" + width + "x" + height + "r" + (cut ? "|imageMogr2/cut/" + width + "x" + height + "/gravity/center" : ""));
        else query.push("imageMogr2/format/jpg/thumbnail/" + width + "x" + height);
    }
    return prefix + "?" + query.join("&");
};

export function selectFile(accept, parent) {
    if (parent) { } else parent = document.body;
    let input = parent.appendChild(document.createElement("input"));
    return new Promise((resolve) => {
        input.setAttribute("type", "file");
        input.setAttribute("accept", string(accept) || "*/*");
        input.setAttribute("style", "position:absolute;top:0;left:0;width:0;height:0;font-size:0;line-height:0;overflow:hidden;");
        input.onchange = () => {
            let file = input.files[0];
            input.onchange = null;
            input.remove();
            resolve(file);
        };
        input.click();
    });
};

export function readFile(file, type) {
    if (!isString(type)) type = string(type);
    let reader = new FileReader();
    return new Promise((resolve) => {
        if (file) reader.onload = reader.onerror = () => resolve(reader.result);
        if (type.toLowerCase() === "buffer") reader.readAsArrayBuffer(file);
        else if (type.toLowerCase() === "url") reader.readAsDataURL(file);
        else reader.readAsText(file);
    }).catch((error) => { });
};

export function getURLQuery(...args) {
    if (args.length > 1) {
        return args.reduce((query, arg) => Object.assign(query, getURLQuery(arg)), {});
    } else if (args.length > 0) {
        let search = string(args[0]).split("#")[0].split("?")[1];
        return Object.assign({}, object(args[0]), string(search).split("&").reduce((query, data) => {
            let [key, value] = data.split("=").concat("");
            if (key.length > 0) query[key] = string(() => decodeURIComponent(value));
            return query;
        }, {}));
    } else {
        return getURLQuery(() => location.href);
    }
};

export function getURLQueryString(...args) {
    let query = args.reduce((query, arg) => Object.assign(query, getURLQuery(arg)), {});
    return Object.entries(query).reduce((list, [key, value]) => {
        if (value === undefined || value === null) key = "";
        else value = string(() => encodeURIComponent(string(value)));
        if (key.length > 0) list.push(key + "=" + value);
        return list;
    }, []).join("&");
};

export function mergeURL(...args) {
    let prefix = "";
    let search = "";
    let hash = "";
    for (let arg of args) {
        if (isObject(arg)) {
            search = getURLQueryString("?" + search, arg);
        } else {
            prefix = string(arg).split("#")[0].split("?")[0] || prefix;
            search = getURLQueryString("?" + search, arg);
            hash = string(arg).split("#")[1] || hash;
        }
    }
    if (search) search = "?" + search;
    if (hash) hash = "#" + hash;
    return prefix + search + hash;
};

export function verifyPassword(value, minLength, minTypes) {
    let password = string(value);
    let length = Math.min(Math.max(int(minLength), 1), 32);
    let types = Math.min(Math.max(int(minTypes), 1), 4);
    let type1 = /[A-Z]/;
    let type2 = /[a-z]/;
    let type3 = /[0-9]/;
    let type4 = /[\u0021-\u002f]|[\u003a-\u0040]|[\u005b-\u0060]|[\u007b-\u007e]/;
    return password.length >= length && [type1, type2, type3, type4].filter((type) => type.test(password)).length >= types;
};

export const caches = new Map();

export function cache(...args) {
    let key = isArray(args[0]) ? args[0].join(".") : string(args[0]);
    let value = args[1];
    let length = args.length;
    if (key.length === 0) return;
    if (length >= 2) caches.set(key, value);
    return caches.get(key);
};

export function session(...args) {
    let key = isArray(args[0]) ? args[0].join(".") : string(args[0]);
    let value = isObject(args[1]) || isArray(args[1]) ? string(() => JSON.stringify(args[1])) : string(args[1]);
    let length = args.length;
    if (key.length === 0) return "";
    if (length >= 2 && value) string(() => sessionStorage.setItem(key, value));
    else if (length >= 2) string(() => sessionStorage.removeItem(key));
    return string(() => sessionStorage.getItem(key));
};

export function storage(...args) {
    let key = isArray(args[0]) ? args[0].join(".") : string(args[0]);
    let value = isObject(args[1]) || isArray(args[1]) ? string(() => JSON.stringify(args[1])) : string(args[1]);
    let length = args.length;
    let setData = () => string(() => uni.setStorageSync(key, value) || key) || string(() => wx.setStorageSync(key, value) || key) || string(() => localStorage.setItem(key, value) || key);
    let getData = () => string(() => uni.getStorageSync(key)) || string(() => wx.getStorageSync(key)) || string(() => localStorage.getItem(key));
    let delData = () => string(() => uni.removeStorageSync(key)) || string(() => wx.removeStorageSync(key)) || string(() => localStorage.removeItem(key));
    if (key.length === 0) return "";
    if (length >= 2 && value) setData();
    else if (length >= 2) delData();
    return getData();
};

export const clocks = new Map();

export function clock(handle, time) {
    let valid = isFunction(handle);
    let playing = valid && isNumber(time) && time >= 0;
    if (valid) clearTimeout(clocks.get(handle));
    if (playing) clocks.set(handle, setTimeout(() => clock(handle, time) + handle(), time));
    else clocks.delete(handle);
};